package com.fourforfo.fourmall.seckill.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.fourforfo.common.to.mq.SeckillOrderTO;
import com.fourforfo.common.utils.R;
import com.fourforfo.fourmall.seckill.feign.CouponFeignService;
import com.fourforfo.fourmall.seckill.feign.ProductFeignService;
import com.fourforfo.fourmall.seckill.service.SeckillService;
import com.fourforfo.fourmall.seckill.to.SeckillSessionWithSkusTO;
import com.fourforfo.fourmall.seckill.to.SeckillSkuRedisTO;
import com.fourforfo.fourmall.seckill.to.SkuInfoTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @program: xxl-sso-core
 * @description:
 * @author: fourforfo
 * @email: fourforfo@gmail.com
 * @create: 2022-09-14 13:45
 **/
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {


    @Autowired
    CouponFeignService couponFeignService;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    ProductFeignService productFeignService;

    @Autowired
    RedissonClient redissonClient;//redisson框架的客户端发起请求类

    @Autowired
    RabbitTemplate rabbitTemplate;

    private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";//+ sessionId_startTime_endTime
    private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";//+ sessionId_skuId
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";//+ sessionId_商品随机码


    //保存活动信息
    private void saveSessionInfos(List<SeckillSessionWithSkusTO> sessionWithSkusTOs){
        sessionWithSkusTOs.stream().forEach(sessionWithSkusTO -> {
            long startTime = sessionWithSkusTO.getStartTime().getTime();
            long endTime = sessionWithSkusTO.getEndTime().getTime();
            //seckill:sessions:sessionId_startTime_endTime
            String key = SESSIONS_CACHE_PREFIX+sessionWithSkusTO.getId()+"_"+startTime+"_"+endTime;
            //如果redis中没有，才往里面放置该sessionInfo
            if(!stringRedisTemplate.hasKey(key)){
                List<String> skuIds = sessionWithSkusTO.getRelationEntities().stream()
                        .map(item->sessionWithSkusTO.getId()+"_"+item.getSkuId().toString())
                        .collect(Collectors.toList());
                stringRedisTemplate.opsForList().leftPushAll(key,skuIds);
            }
        });
    }

    //保存活动商品信息
    private void saveSessionSkuInfos(List<SeckillSessionWithSkusTO> sessionWithSkusTOs){

        //key为sessionId，value为sessionId对应的商品id集合
        Map<Long, List<Long>> idsMap = sessionWithSkusTOs.stream().collect(
                Collectors.toMap(
                        //key为sessionId
                        SeckillSessionWithSkusTO::getId,
                        //value为sessionId对应的商品id集合
                        sessionWithSkusTO -> {
                            List<Long> skuIds = sessionWithSkusTO.getRelationEntities().stream()
                                    .map(SeckillSessionWithSkusTO.SeckillSkuRelationTO::getSkuId)
                                    .collect(Collectors.toList());
                            return skuIds;
                        }));

        R r = productFeignService.listSkuInfoBySkuIdGroupBySessionId(idsMap);
        if(r.getCode()!=0){
            log.warn("远程批量查询skuInfo失败");
            return;
        }
        //外层key是sessionId，里层key是skuId
        Map<Long, Map<Long, SkuInfoTO>> sessionIdSkuInfoMap = r.getData(new TypeReference<Map<Long, Map<Long, SkuInfoTO>>>() {
        });


        sessionWithSkusTOs.stream().forEach(sessionWithSkusTO -> {
            BoundHashOperations<String,Object,Object> operations = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            Map<Long, SkuInfoTO> skuInfoTOMap = sessionIdSkuInfoMap.get(sessionWithSkusTO.getId());


            sessionWithSkusTO.getRelationEntities().stream().forEach(skuRelation->{
                //4.随机码？防止用户通过脚本提前对某个秒杀商品通过skuId发送秒杀请求 seckill?skuId=1&key=feit343kjdfskdis
                String randomCodde = UUID.randomUUID().toString().replace("_","");

                String skuKillKey = sessionWithSkusTO.getId()+"_"+skuRelation.getSkuId();//前缀为SKUKILL_CACHE_PREFIX
                //如果redis中没有该秒杀商品的信息，设置该商品sku信息到redis中
                if(!operations.hasKey(skuKillKey)){
                    //缓存商品
                    SeckillSkuRedisTO redisTO = new SeckillSkuRedisTO();

                    //1.sku的基本数据
                    SkuInfoTO skuInfoTO = skuInfoTOMap.get(skuRelation.getSkuId());
                    redisTO.setSkuInfoTO(skuInfoTO);

                    //2.sku的秒杀信息
                    BeanUtils.copyProperties(skuRelation,redisTO);

                    //3.设置当前商品的秒杀时间信息
                    redisTO.setStartTime(sessionWithSkusTO.getStartTime().getTime());
                    redisTO.setEndTime(sessionWithSkusTO.getEndTime().getTime());
                    redisTO.setRandomCode(randomCodde);

                    //发送信号量 seckill:stock:sessionId_商品随机码
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + sessionWithSkusTO.getId() + "_" + randomCodde);

                    //设置商品秒杀件数为一个信号量
                    semaphore.trySetPermits(skuRelation.getSeckillCount().intValue());

                    String redisTOJsonStr = JSON.toJSONString(redisTO);
                    //seckill:skus:sessionId_skuId
                    operations.put(skuKillKey,redisTOJsonStr);
                }

            });
        });
    }

    /**
     *秒杀商品定时上架计时器
     *    每天晚上3点，上架最近三天需要秒杀的商品
     *    当天：00:00:00 - 23:59:59
     *    明天：00:00:00 - 23:59:59
     *    后天：00:00:00 - 23:59:59
     */
    @Override
    public void uploadSeckillSkuLatest3Days() {

        //1.扫描最近三天需要参与秒杀的活动
        //重复上架无需处理

        R r = couponFeignService.getLatest3DaysSession();
        if(r.getCode()==0){
            List<SeckillSessionWithSkusTO> sessionWithSkusTOs = r.getData(new TypeReference<List<SeckillSessionWithSkusTO>>() {
            });
            //缓存到redis中
            //1.缓存活动信息
            saveSessionInfos(sessionWithSkusTOs);
            //2.缓存活动关联商品信息
            saveSessionSkuInfos(sessionWithSkusTOs);
        }

    }

    /**
     * 获取当前参与秒杀的商品信息
     * @return
     */
    @Override
    public List<SeckillSkuRedisTO> getCurrentSeckillSkus() {
        //1.确定当前时间属于哪个秒杀场次
        long nowTime = new Date().getTime();
        Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
        for (String key : keys) {
            //seckill:sessions:3_1663334040000_1663162129000
            int index = key.indexOf("_");
            String start_endTime = key.substring(index + 1);
            String[] s = start_endTime.split("_");
            Long startTime = Long.parseLong(s[0]);
            Long endTime = Long.parseLong(s[1]);
            if(nowTime<=endTime && nowTime>=startTime){
                List<String> skuKeys = stringRedisTemplate.opsForList().range(key, 0, -1);
                BoundHashOperations<String, String, String> operations =
                        stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                List<String> values = operations.multiGet(skuKeys);
                if(values!=null && values.size()>0){
                    List<SeckillSkuRedisTO> result = values.stream().map(value -> {
                        SeckillSkuRedisTO seckillSkuRedisTO = JSON.parseObject(value, new TypeReference<SeckillSkuRedisTO>() {
                        });
                        return seckillSkuRedisTO;
                    }).collect(Collectors.toList());
                    return result;
                }
            }
        }

        //2.获取这个秒杀场次需要的所有商品信息
//        stringRedisTemplate.opsForValue().get()

        return null;
    }

    /**
     * 获取某商品的秒杀信息，即是否是秒杀商品，以及秒杀的上次等等
     */
    @Override
    public SeckillSkuRedisTO getSkuSeckillInfo(Long skuId) {
        //1.找到所有需要参与秒杀的sku信息

        String skuIdStr = skuId.toString();
        BoundHashOperations<String, String, String> operations =
                stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        Set<String> keys = operations.keys();

        if(keys!=null && keys.size()>0){
            for (String key : keys) {
                if(skuIdStr.equals(key.split("_")[1])){
                    String valueStr = operations.get(key);
                    SeckillSkuRedisTO redisTO = JSON.parseObject(valueStr,new TypeReference<SeckillSkuRedisTO>(){});
                    LocalDateTime nowDate = LocalDateTime.now();
                    LocalDateTime twoLaterDate = nowDate.plusDays(2);

                    //将redisTo的开始时间转化为LocalDateTime格式
                    LocalDateTime startDateTime = LocalDateTime.ofInstant(new Date(redisTO.getStartTime()).toInstant(), ZoneId.systemDefault());
                    LocalDateTime endDateTime = LocalDateTime.ofInstant(new Date(redisTO.getEndTime()).toInstant(),ZoneId.systemDefault());
                    /*
                    如果商品秒杀开始时间在今天之后且在2天后之前，
                        那么就预报该商品将会秒杀，
                        即预报两天内将要秒杀的商品
                    如果商品秒杀已经开始，那么也展示
                     */
                    if(twoLaterDate.isBefore(startDateTime) || endDateTime.isAfter(nowDate)){
                        return redisTO;
                    }

                }
            }
        }

        return null;
    }

    /**
     * 秒杀商品
     * @param killId 秒杀id，sessionId_skuId组成 ，sessionId为秒杀活动号
     * @param randomCode 随机码
     * @param num 数量
     * @param userid
     * @return
     */
    @Override
    public R seckill(String killId, String randomCode, Integer num, String userid){
        //1.获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> operations = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);

        String valueStr = operations.get(killId);
        if(StringUtils.isEmpty(valueStr)){
            log.warn("获取不到秒杀信息，检查是否是killId错误！");
            return R.error("获取不到秒杀信息，检查是否是killId错误！");
        }
        SeckillSkuRedisTO redisTO = JSON.parseObject(valueStr,new TypeReference<SeckillSkuRedisTO>(){});
        //2.校验时间合法性
        Long startTime = redisTO.getStartTime();
        Long endTime = redisTO.getEndTime();
        Long nowTime = new Date().getTime();
        if(nowTime<startTime || nowTime>endTime){
            log.warn("当前时间与活动秒杀时间不符！");
            return R.error("当前时间与活动秒杀时间不符！");
        }

        //3.检验随机码是否匹配
        if(!StringUtils.equals(randomCode,redisTO.getRandomCode())){
            log.warn("唯一验证码校验错误！");
            return R.error("唯一验证码校验错误！");
        }

        //4.判断用户是否已经发送过此抢购请求，保证幂等性 key = userId_sessionId_skuId
        String isBoughtKey = userid+"_"+redisTO.getPromotionSessionId()+"_"+redisTO.getSkuId();
        /*
        按此key用做redis占位，key = userId_sessionId_skuId
            如果占位成功，num将会保存到redis中，
            如果占位失败，说明该用户之前站过位，本次请求是重复请求

        设置过期时间，过期时间为该场次时间，这样的话该请求在一场秒杀中只能成功下单一次
         */
        Long ttl = endTime - nowTime;
        Boolean isSet = stringRedisTemplate.opsForValue().setIfAbsent(isBoughtKey, num.toString(),ttl, TimeUnit.MILLISECONDS);
        if(!isSet){//该场次第一次发请求
            log.warn("已进行过秒杀，请求重复！");
            return R.error("已进行过秒杀，请求重复！");
        }

        //5.获取信号量
        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + redisTO.getPromotionSessionId() + "_" + redisTO.getRandomCode());
        Boolean isSuccess = false;
        try {
            isSuccess = semaphore.tryAcquire(num,100, TimeUnit.MILLISECONDS);//等待100ms
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //如果获取不到到库存信号量，说明要么随机码错误，要么库存不足
        if(!isSuccess){
            log.warn("获取信号量失败，库存不足!");
            return R.error("获取信号量失败，库存不足!");
        }
        /**
         * 发送下单请求给MQ，由库存服务监听该消息服务，然后一一下单
         *   随机生成一个订单号，发送给消息队列，
         *   然后直接返回订单号到前端
         */
        String orderSn = UUID.randomUUID().toString();

        SeckillOrderTO orderTO = new SeckillOrderTO();
        orderTO.setNum(num);
        orderTO.setSkuId(redisTO.getSkuId());
        orderTO.setOrderSn(orderSn);
        orderTO.setMemberId(userid);
        orderTO.setSeckillPrice(redisTO.getSeckillPrice());
        orderTO.setPromotionSessionId(redisTO.getPromotionSessionId());

        //6.发送下单信息给MQ
        rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTO);

        return R.ok(orderSn);
    }
}
